package log

import (
	"emma/platform"
	"errors"
	"fmt"
	"io/ioutil"
	"log"
	"os"
	"path/filepath"
	"runtime"
	"strings"
	"time"
)

// levels
type LogLevel = int

type Function = func(format string, args ...interface{})

const (
	LogLevel_Debug = LogLevel(iota)
	LogLevel_Release
	LogLevel_Warning
	LogLevel_Error
	LogLevel_Fatal
	maxLevel
)

var str2logLevels = map[string]LogLevel{
	"debug":   LogLevel_Debug,
	"release": LogLevel_Release,
	"warning": LogLevel_Warning,
	"error":   LogLevel_Error,
	"fatal":   LogLevel_Fatal,
}

var level2Prefixes = map[LogLevel]string{
	LogLevel_Debug:   "[debug  ] ",
	LogLevel_Release: "[release] ",
	LogLevel_Warning: "[warning] ",
	LogLevel_Error:   "[error  ] ",
	LogLevel_Fatal:   "[fatal  ] ",
}

type Logger struct {
	level   LogLevel
	base    *log.Logger
	file    *os.File
	loggers []*log.Logger
}

func New(strLevel string, logPath string, logFlag int) (result *Logger, err error) {
	// level
	strLevel = strings.ToLower(strLevel)
	level, exists := str2logLevels[strLevel]
	if !exists {
		err = errors.New("unknown level: " + strLevel)
		return
	}

	now := time.Now()
	filename := fmt.Sprintf("%d-%02d-%02d %02d_%02d_%02d.log",
		now.Year(),
		now.Month(),
		now.Day(),
		now.Hour(),
		now.Minute(),
		now.Second(),
	)

	result = new(Logger)
	result.level = level

	if logPath != "" {
		if _, err = ioutil.ReadDir(logPath); err != nil {
			if err = os.MkdirAll(logPath, os.ModePerm); err != nil {
				return
			}
		}

		path := filepath.Join(logPath, filename)
		if result.file, err = os.OpenFile(path, os.O_CREATE|os.O_APPEND|os.O_WRONLY, os.ModeAppend); err != nil {
			return
		}

		result.base = log.New(result.file, "", logFlag)
	}

	for i := level; i < maxLevel; i++ {
		item := log.New(os.Stdout, level2Prefixes[i], logFlag)
		result.loggers = append(result.loggers, item)
	}

	return
}

// It's dangerous to call the method on logging
func (logger *Logger) Close() {
	if logger.file != nil {
		logger.file.Close()
	}

	logger.base = nil
	logger.file = nil
	logger.loggers = nil
	logger.level = maxLevel
}

func (logger *Logger) SetLevel(level int) {
	logger.level = level
}

func (logger *Logger) doPrintf(level LogLevel, text string, args ...interface{}) {
	if level < logger.level {
		return
	}

	item := logger.loggers[level-logger.level]
	if item == nil {
		return
	}

	if len(args) > 0 {
		text = fmt.Sprintf(text, args...)
	}

	if level >= LogLevel_Error {
		buf := make([]byte, 0xffff)
		count := runtime.Stack(buf, false)
		buf = buf[:count]
		stack := string(buf)
		text = fmt.Sprintf("%s%s%s",
			text,
			platform.EndLine(),
			stack,
		)
	}

	item.Output(3, text)
	if logger.file != nil {
		text = level2Prefixes[level] + text
		logger.base.Output(3, text)
	}


	if level >= LogLevel_Fatal {
		os.Exit(1)
	}
}

func (logger *Logger) Level() LogLevel {
	return logger.level
}

func (logger *Logger) Print(level LogLevel, text string) {
	logger.doPrintf(level, text)
}

func (logger *Logger) Debug(format string, a ...interface{}) {
	logger.doPrintf(LogLevel_Debug, format, a...)
}

func (logger *Logger) Release(format string, a ...interface{}) {
	logger.doPrintf(LogLevel_Release, format, a...)
}

func (logger *Logger) Warning(format string, a ...interface{}) {
	logger.doPrintf(LogLevel_Warning, format, a...)
}

func (logger *Logger) Error(format string, a ...interface{}) {
	logger.doPrintf(LogLevel_Error, format, a...)
}

func (logger *Logger) Fatal(format string, a ...interface{}) {
	logger.doPrintf(LogLevel_Fatal, format, a...)
}

var gLogger, _ = New("debug", "", log.LstdFlags)

// It's dangerous to call the method on logging
func Export(logger *Logger) {
	if logger != nil {
		gLogger = logger
	}
}

func Debug(format string, a ...interface{}) {
	gLogger.doPrintf(LogLevel_Debug, format, a...)
}

func Release(format string, a ...interface{}) {
	gLogger.doPrintf(LogLevel_Release, format, a...)
}

func Warning(format string, a ...interface{}) {
	gLogger.doPrintf(LogLevel_Warning, format, a...)
}

func Error(format string, a ...interface{}) {
	gLogger.doPrintf(LogLevel_Error, format, a...)
}

func Fatal(format string, a ...interface{}) {
	gLogger.doPrintf(LogLevel_Fatal, format, a...)
}

func Close() {
	gLogger.Close()
}
